1
|
|
|
/* |
2
|
|
|
* decimal.js-i18n v0.2.6 |
3
|
|
|
* Full internationalization support for decimal.js. |
4
|
|
|
* MIT License |
5
|
|
|
* Copyright (c) 2022 Pedro José Batista <[email protected]> |
6
|
|
|
* https://github.com/pjbatista/decimal.js-i18n |
7
|
|
|
*/ |
8
|
1 |
|
import Decimal from "decimal.js"; |
9
|
|
|
import type BaseFormatOptions from "./baseOptions"; |
10
|
|
|
import type FormatCompactDisplay from "./compactDisplay"; |
11
|
1 |
|
import { BIGINT_MODIFIERS, ECMA_LIMIT, LOCALES, PLAIN_MODIFIERS } from "./constants"; |
12
|
|
|
import type FormatCurrency from "./currency"; |
13
|
|
|
import type FormatCurrencyDisplay from "./currencyDisplay"; |
14
|
|
|
import type FormatCurrencySign from "./currencySign"; |
15
|
|
|
import type FormatLocale from "./locale"; |
16
|
|
|
import type FormatLocaleMatcher from "./localeMatcher"; |
17
|
|
|
import type FormatNotation from "./notation"; |
18
|
|
|
import type FormatNumberingSystem from "./numberingSystem"; |
19
|
|
|
import type FormatOptions from "./options"; |
20
|
1 |
|
import { extend, resolve, toEcma, validate } from "./options"; |
21
|
|
|
import type FormatPart from "./part"; |
22
|
1 |
|
import { exponents, fractions, integerGroups, integers, PartValue } from "./part"; |
23
|
|
|
import type FormatPartTypes from "./partTypes"; |
24
|
|
|
import type ResolvedFormatOptions from "./resolvedFormatOptions"; |
25
|
|
|
import type FormatSignDisplay from "./signDisplay"; |
26
|
|
|
import type FormatStyle from "./style"; |
27
|
|
|
import type FormatTrailingZeroDisplay from "./trailingZeroDisplay"; |
28
|
|
|
import type FormatUnit from "./unit"; |
29
|
|
|
import type FormatUnitDisplay from "./unitDisplay"; |
30
|
|
|
import type FormatUseGrouping from "./useGrouping"; |
31
|
|
|
|
32
|
|
|
// Calculates an exponential value using base₁₀ |
33
|
1 |
|
const defaultLocales = LOCALES.slice(); |
34
|
|
|
|
35
|
1 |
|
const concatenate = <T extends PartValue>(filter: T[] | ((p: T) => boolean), parts: T[] = []) => { |
36
|
82582 |
|
if (typeof filter === "function") { |
37
|
82568 |
|
parts = parts.filter(filter); |
38
|
|
|
} else { |
39
|
14 |
|
parts = filter; |
40
|
|
|
} |
41
|
|
|
|
42
|
82582 |
|
return parts.map(p => p.value).join(""); |
43
|
|
|
}; |
44
|
|
|
|
45
|
5520 |
|
const pow10 = (exponent: Decimal.Value) => Decimal.pow(10, exponent); |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* The `Decimal.Format` object enables language-sensitive decimal number formatting. It is entirely based on |
49
|
|
|
* `Intl.NumberFormat`, with the options of the latter being 100% compatible with it. |
50
|
|
|
* |
51
|
|
|
* This class, however, extend the numeric digits constraints of `Intl.NumberFormat` from 21 to 1000000000 in |
52
|
|
|
* order to fully take advantage of the arbitrary-precision of `decimal.js`. |
53
|
|
|
* |
54
|
|
|
* @template TNotation Numeric notation of formatting. |
55
|
|
|
* @template TStyle Numeric style of formatting. |
56
|
|
|
*/ |
57
|
1 |
|
export class Format<TNotation extends FormatNotation = "standard", TStyle extends FormatStyle = "decimal"> { |
58
|
1 |
|
static readonly [Symbol.toPrimitive] = Format; |
59
|
1169 |
|
readonly [Symbol.toStringTag] = "Decimal.Format"; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Formats a number according to the locale and formatting options of this {@link Format} object. |
63
|
|
|
* |
64
|
|
|
* @param value A valid [Decimal.Value](https://mikemcl.github.io/decimal.js/#decimal) to format. |
65
|
|
|
* @returns Formatted localized string. |
66
|
|
|
*/ |
67
|
|
|
readonly format: (value: Decimal.Value) => string; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Allows locale-aware formatting of strings produced by `Decimal.Format` formatters. |
71
|
|
|
* |
72
|
|
|
* @param value A valid [Decimal.Value](https://mikemcl.github.io/decimal.js/#decimal) to format. |
73
|
|
|
* @returns An array of objects containing the formatted number in parts. |
74
|
|
|
*/ |
75
|
|
|
readonly formatToParts: (value: Decimal.Value) => FormatPart[]; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Returns a new object with properties reflecting the locale and number formatting options computed during |
79
|
|
|
* initialization of this {@link Decimal.Format} object. |
80
|
|
|
* |
81
|
|
|
* @returns A new object with properties reflecting the locale and number formatting options computed |
82
|
|
|
* during the initialization of this object. |
83
|
|
|
*/ |
84
|
|
|
readonly resolvedOptions: () => ResolvedFormatOptions<TNotation, TStyle>; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Creates a new instance of the `Decimal.Format` object. |
88
|
|
|
* |
89
|
|
|
* @param locales A string with a [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag, or an array |
90
|
|
|
* of such strings. |
91
|
|
|
* |
92
|
|
|
* For the general form and interpretation of this parameter, see the [Intl page on |
93
|
|
|
* MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). |
94
|
|
|
* @param options Object used to configure the behavior of the string localization. |
95
|
|
|
* @throws `RangeError` when an invalid option is given. |
96
|
|
|
*/ |
97
|
|
|
constructor(locales?: FormatLocale | FormatLocale[], options?: FormatOptions<TNotation, TStyle>) { |
98
|
1169 |
|
options ??= {}; |
99
|
|
|
|
100
|
|
|
// 1. Check if options do not extrapolate the limits of decimal.js |
101
|
1169 |
|
const valid = validate(options); |
102
|
|
|
|
103
|
1169 |
|
if (valid !== true) { |
104
|
|
|
// -> it will either be exactly true or contain an array with all faulty properties: |
105
|
5 |
|
throw new RangeError(`${valid.join()} value${valid.length === 1 ? " is" : "s are"} out of range."`); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
// 2. Create a baseline native formatter native |
109
|
1164 |
|
const ecmaOptions = toEcma(options); |
110
|
1164 |
|
const ecmaFormat = new Intl.NumberFormat(locales, ecmaOptions); |
111
|
|
|
|
112
|
|
|
// 3. Resolve this object's options, using the native resolution as a baseline |
113
|
1164 |
|
const resolved = resolve(options, ecmaFormat.resolvedOptions()); |
114
|
1164 |
|
const { minimumIntegerDigits: minID, notation, rounding, style } = resolved; |
115
|
|
|
|
116
|
|
|
// 4. Create two auxiliary formatters: |
117
|
|
|
// One for the integer part, which can have up to a billion minimum digits... |
118
|
1164 |
|
const bigintOptions = extend(ecmaOptions, BIGINT_MODIFIERS); |
119
|
1164 |
|
const bigintFormat = new Intl.NumberFormat(locales, bigintOptions); |
120
|
|
|
|
121
|
|
|
// ...and another for a plain, localized reference, used for decimals and constants |
122
|
1164 |
|
const plainOptions = extend(bigintOptions, PLAIN_MODIFIERS); |
123
|
1164 |
|
const plainFormat = new Intl.NumberFormat(locales, plainOptions); |
124
|
|
|
|
125
|
|
|
// 5. Localized numeric constants |
126
|
1164 |
|
const numbers = Array(10) |
127
|
|
|
.fill(null) |
128
|
11640 |
|
.map((_, index) => plainFormat.format(index)); |
129
|
1164 |
|
const numberMatch = new RegExp("[" + numbers.join("") + "]", "g"); |
130
|
1164 |
|
const minusSign = /−/gu; |
131
|
|
|
|
132
|
|
|
// 5.1. Localized zero and one used in substitutions |
133
|
1164 |
|
const [zero, one] = numbers; |
134
|
|
|
|
135
|
|
|
// 5.2. Helper functions |
136
|
4590 |
|
const indexOfValue = (value: string) => numbers.indexOf(value).toString(); |
137
|
4584 |
|
const convert = (text: string) => text.replaceAll(numberMatch, indexOfValue).replaceAll(minusSign, "-"); |
138
|
1164 |
|
const zeroFill = (size: number) => Array(size).fill(zero).join(""); |
139
|
1164 |
|
const zeroTrim = (text: string) => { |
140
|
20642 |
|
let result = text; |
141
|
|
|
|
142
|
20642 |
|
while (result[0] === zero && result.length > 1) { |
143
|
335 |
|
result = result.substring(1); |
144
|
|
|
} |
145
|
|
|
|
146
|
20642 |
|
return result; |
147
|
|
|
}; |
148
|
|
|
|
149
|
|
|
// #region Step 6. Main format method - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
150
|
1164 |
|
const _formatToParts = (value: Decimal.Value) => { |
151
|
25226 |
|
value = new Decimal(value); |
152
|
25226 |
|
const sign = value.s; |
153
|
|
|
|
154
|
|
|
// 6.1. Create a baseline part array |
155
|
25226 |
|
const ecmaParts = ecmaFormat.formatToParts(value.toNumber()); |
156
|
|
|
|
157
|
|
|
// -> if the value is non-numeric or an infinity, the baseline is good enough |
158
|
25226 |
|
if ((value.isFinite && !value.isFinite()) || (value.isNaN && value.isNaN())) { |
159
|
4584 |
|
return ecmaParts; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
// 6.2. Splitting the parts for easier assembly |
163
|
20642 |
|
const ecmaExponentValue = concatenate(exponents, ecmaParts) || "0"; |
164
|
20642 |
|
const ecmaIntegerParts = ecmaParts.filter(integerGroups); |
165
|
20642 |
|
const ecmaIntegerTrimmed = zeroTrim(concatenate(integers, ecmaIntegerParts)); |
166
|
20642 |
|
const ecmaIntegerDigits = concatenate(integers, ecmaIntegerParts).length; |
167
|
20642 |
|
const ecmaIntegerTrimmedDigits = ecmaIntegerTrimmed.length; |
168
|
20642 |
|
const ecmaFractionValue = concatenate(fractions, ecmaParts); |
169
|
20642 |
|
const ecmaFractionDigits = ecmaFractionValue.length; |
170
|
|
|
|
171
|
|
|
// 6.3. Shifting exponents according to notation/style |
172
|
|
|
|
173
|
|
|
// 6.3.1. Compact notation: calculate the shift in integer digits, and therefore exponent |
174
|
20642 |
|
if (notation === "compact" && !value.eq(0)) { |
175
|
3664 |
|
const baseInteger = value.abs().trunc().toFixed(); |
176
|
3664 |
|
const baseIntegerDigits = baseInteger.length; |
177
|
3664 |
|
const correctionDigits = baseIntegerDigits - ecmaIntegerTrimmedDigits; |
178
|
|
|
|
179
|
3664 |
|
if (correctionDigits > 0) { |
180
|
888 |
|
value = value.mul(pow10(-correctionDigits)); |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
// 6.3.2. Engr./Scientific notations: evaluate the exponent from the text |
185
|
20642 |
|
if ((notation === "engineering" || notation === "scientific") && ecmaExponentValue !== zero) { |
186
|
4584 |
|
const exponential = new Decimal(convert(ecmaExponentValue)); |
187
|
4584 |
|
value = value.mul(pow10(exponential.mul(-1))).abs().mul(sign); // prettier-ignore |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// 6.3.3. Percent style: shift the value accordingly (non numeric parts will remain the same) |
191
|
20642 |
|
if (style === "percent") value = value.mul(100); |
192
|
|
|
|
193
|
|
|
// 6.4. Parsing the information about the numeric parts |
194
|
20642 |
|
const integer = value.abs().trunc().mul(sign); |
195
|
20642 |
|
const fraction = value.sub(integer).abs(); |
196
|
20642 |
|
const integerDigits = !value.eq(0) && integer.eq(0) ? 0 : value.abs().trunc().toFixed().length; |
197
|
20642 |
|
const fractionDigits = value.dp(); |
198
|
20642 |
|
const maxSD = resolved.maximumSignificantDigits ?? resolved.maximumFractionDigits! + integerDigits; |
199
|
20642 |
|
const maxFD = resolved.maximumFractionDigits ?? maxSD - integerDigits; |
200
|
20642 |
|
const minSD = resolved.minimumSignificantDigits ?? resolved.minimumFractionDigits! + integerDigits; |
201
|
20642 |
|
const minFD = resolved.minimumFractionDigits ?? minSD - integerDigits; |
202
|
|
|
|
203
|
|
|
// 6.5. Check for the possibility of the native formatter to have accomplished the desired output |
204
|
20642 |
|
const integerCheck = !ecmaIntegerParts.length || (minID <= ECMA_LIMIT && ecmaIntegerDigits >= minID); |
205
|
20642 |
|
const fractionCheck = !ecmaFractionDigits || (minFD < ECMA_LIMIT && ecmaFractionDigits >= minFD); |
206
|
|
|
|
207
|
|
|
// -> if the native formatter is good enough for our decimal value, leave it as-is |
208
|
20642 |
|
if (integerCheck && fractionCheck) { |
209
|
20612 |
|
return ecmaParts as FormatPart[]; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// 6.6. Create the integer value |
213
|
30 |
|
const integerParts = (() => { |
214
|
30 |
|
if (integerCheck) return ecmaIntegerParts; |
215
|
|
|
|
216
|
|
|
// Expanding the integer part |
217
|
19 |
|
const targetDigits = Math.max(integerDigits, minID); |
218
|
|
|
|
219
|
|
|
// Creates a base 10 power of the target digits |
220
|
19 |
|
const bigint = BigInt(pow10(targetDigits - 1).toFixed()); |
221
|
|
|
|
222
|
|
|
// Format using the bigint formatter and cut it before joining with the ECMA parts |
223
|
19 |
|
const bigintIntegerParts = bigintFormat.formatToParts(bigint).filter(integerGroups); |
224
|
|
|
|
225
|
|
|
// We need to replace the first 'one' (from the base 10 power) with a 'zero' |
226
|
19 |
|
bigintIntegerParts[0].value = bigintIntegerParts[0].value.replace(new RegExp(one), zero); |
227
|
|
|
|
228
|
|
|
// Merge the first part with the bigint part |
229
|
19 |
|
ecmaIntegerParts[0].value = |
230
|
|
|
bigintIntegerParts[ecmaIntegerParts.length - 1].value.slice(0, -ecmaIntegerParts[0].value.length) + |
231
|
|
|
ecmaIntegerParts[0].value; |
232
|
|
|
|
233
|
19 |
|
return [...bigintIntegerParts.slice(0, -ecmaIntegerParts.length), ...ecmaIntegerParts]; |
234
|
|
|
})(); |
235
|
|
|
|
236
|
|
|
// 6.7. Create the fraction value |
237
|
30 |
|
const fractionValue = (() => { |
238
|
30 |
|
if (fractionCheck) return ecmaFractionValue; |
239
|
|
|
|
240
|
|
|
// Simpler formatting if there is actually no fraction |
241
|
29 |
|
if (fraction.eq(0)) { |
242
|
13 |
|
return plainFormat.format(BigInt(pow10(minFD).toFixed())).slice(1); |
243
|
|
|
} |
244
|
|
|
|
245
|
16 |
|
let suffix = ""; |
246
|
|
|
|
247
|
|
|
// Exponential value of the fraction (converting from decimal to bigint) |
248
|
16 |
|
const exponential = fraction.toDP(maxFD, rounding).mul(pow10(maxFD)).toFixed(); |
249
|
|
|
|
250
|
|
|
// First, create a zero-filled right-side expansion if the digits are insufficient |
251
|
16 |
|
if (fractionDigits < minFD) { |
252
|
15 |
|
suffix = zeroFill(minFD - maxFD); |
253
|
|
|
} |
254
|
|
|
|
255
|
16 |
|
const fractionValue = plainFormat.format(BigInt(exponential)) + suffix; |
256
|
|
|
|
257
|
|
|
// If the value is still not enough, it needs more left-zero-filling |
258
|
16 |
|
if (fractionValue.length < minFD) { |
259
|
2 |
|
return zeroFill(minFD - fractionValue.length) + fractionValue; |
260
|
|
|
} |
261
|
|
|
// If it's bigger than the maximum, slice it |
262
|
14 |
|
else if (fractionValue.length > maxFD) { |
263
|
|
|
return fractionValue.slice(0, maxFD); |
264
|
|
|
} |
265
|
|
|
|
266
|
14 |
|
return fractionValue; |
267
|
|
|
})(); |
268
|
|
|
|
269
|
|
|
// 6.8. Parsing the numeric fragments in a unified part array |
270
|
30 |
|
const result: FormatPart[] = []; |
271
|
30 |
|
let integerDone = false; |
272
|
30 |
|
let fractionDone = false; |
273
|
|
|
|
274
|
30 |
|
while (ecmaParts.length) { |
275
|
383 |
|
const { type, value } = ecmaParts.shift()!; |
276
|
|
|
|
277
|
383 |
|
if (type === "integer" || type === "group") { |
278
|
280 |
|
if (!integerDone) { |
279
|
30 |
|
integerDone = true; |
280
|
30 |
|
result.push(...integerParts); |
281
|
|
|
} |
282
|
280 |
|
continue; |
283
|
|
|
} |
284
|
|
|
|
285
|
103 |
|
if (type === "fraction") { |
286
|
29 |
|
if (!fractionDone) { |
287
|
29 |
|
fractionDone = true; |
288
|
29 |
|
result.push({ type, value: fractionValue }); |
289
|
|
|
} |
290
|
29 |
|
continue; |
291
|
|
|
} |
292
|
|
|
|
293
|
74 |
|
result.push({ type, value }); |
294
|
|
|
} |
295
|
30 |
|
return result; |
296
|
|
|
}; |
297
|
|
|
//#endregion |
298
|
|
|
|
299
|
1164 |
|
this.format = value => concatenate(_formatToParts(value)); |
300
|
25212 |
|
this.formatToParts = value => _formatToParts(value); |
301
|
1164 |
|
this.resolvedOptions = () => ({ ...resolved }); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Returns an array containing the default locales available to the environment, based on a default |
306
|
|
|
* dictionary of locales and regions. |
307
|
|
|
* |
308
|
|
|
* **Note:** This method is non-standard and not available on `Intl` formatters. |
309
|
|
|
* |
310
|
|
|
* @returns Array of strings with the available locales. |
311
|
|
|
*/ |
312
|
|
|
static supportedLocales(): FormatLocale[] { |
313
|
2 |
|
return Intl.NumberFormat.supportedLocalesOf(defaultLocales); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Returns an array containing those of the provided locales that are supported without having to fall back |
318
|
|
|
* to the runtime's default locale. |
319
|
|
|
* |
320
|
|
|
* @template TNotation Numeric notation of formatting. |
321
|
|
|
* @template TStyle Numeric style of formatting. |
322
|
|
|
* @param locales A string with a BCP 47 language tag, or an array of such strings. For the general form |
323
|
|
|
* and interpretation of the locales argument, see the [Intl page on |
324
|
|
|
* MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). |
325
|
|
|
* @param options Object used to configure the behavior of the string localization. |
326
|
|
|
* @returns Array of strings with the available locales. |
327
|
|
|
*/ |
328
|
|
|
static supportedLocalesOf<TNotation extends FormatNotation = "standard", TStyle extends FormatStyle = "decimal">( |
329
|
|
|
locales: string | string[], |
330
|
|
|
options?: FormatOptions<TNotation, TStyle>, |
331
|
|
|
) { |
332
|
2 |
|
return Intl.NumberFormat.supportedLocalesOf(locales, options ? toEcma(options) : undefined) as FormatLocale[]; |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace |
336
|
|
|
export declare namespace Format { |
337
|
|
|
export type { |
338
|
|
|
BaseFormatOptions, |
339
|
|
|
FormatCompactDisplay, |
340
|
|
|
FormatCurrency, |
341
|
|
|
FormatCurrencyDisplay, |
342
|
|
|
FormatCurrencySign, |
343
|
|
|
FormatLocale, |
344
|
|
|
FormatLocaleMatcher, |
345
|
|
|
FormatNotation, |
346
|
|
|
FormatNumberingSystem, |
347
|
|
|
FormatOptions, |
348
|
|
|
FormatPart, |
349
|
|
|
FormatPartTypes, |
350
|
|
|
ResolvedFormatOptions, |
351
|
|
|
FormatSignDisplay, |
352
|
|
|
FormatStyle, |
353
|
|
|
FormatTrailingZeroDisplay, |
354
|
|
|
FormatUnit, |
355
|
|
|
FormatUnitDisplay, |
356
|
|
|
FormatUseGrouping, |
357
|
|
|
}; |
358
|
|
|
} |
359
|
|
|
export type { |
360
|
|
|
BaseFormatOptions, |
361
|
|
|
FormatCompactDisplay, |
362
|
|
|
FormatCurrency, |
363
|
|
|
FormatCurrencyDisplay, |
364
|
|
|
FormatCurrencySign, |
365
|
|
|
FormatLocale, |
366
|
|
|
FormatLocaleMatcher, |
367
|
|
|
FormatNotation, |
368
|
|
|
FormatNumberingSystem, |
369
|
|
|
FormatOptions, |
370
|
|
|
FormatPart, |
371
|
|
|
FormatPartTypes, |
372
|
|
|
ResolvedFormatOptions, |
373
|
|
|
FormatSignDisplay, |
374
|
|
|
FormatStyle, |
375
|
|
|
FormatTrailingZeroDisplay, |
376
|
|
|
FormatUnit, |
377
|
|
|
FormatUnitDisplay, |
378
|
|
|
FormatUseGrouping, |
379
|
|
|
}; |
380
|
|
|
|
381
|
|
|
export default Format; |
382
|
|
|
|